"""
JIRA XMLRPC Wrapper

Wraps the JIRA XMLRPC Server
to make the interface simpler and more 'pythonic'.

Wraps the various XMLRPC calls
so that they return full-featured Python objects
instead of dictionaries.
"""

import sys
import os
import xmlrpclib
import getpass
import mx.DateTime
import optparse


# -- MONKEY PATCH TIME! --
# Overwrite decoding function
# used by the default Unmarshaller to
# change the default encoding
import re
def decode(data, encoding, is8bit=re.compile("[\x80-\xff]").search):
    # decode non-ascii string (if possible)
    if unicode and encoding and is8bit(data):
        data = unicode(data, "ISO-8859-1")
    return data
xmlrpclib._decode = decode
# -- END MONKEY PATCH! --

class Wrappable(object):
    def __init__(self, server, dict):
        self._keys = dict.keys()
        self._server = server
        self.__dict__.update(dict)

    def __getattr__(self, key):
        if key in self.__dict__:
            return self.__dict__[key]
        return self.__getattribute__(key)

    def __getitem__(self, key):
        return self.__dict__[key]

    def __str__(self):
        s = "<%s %s>"
        values = []
        for key in self._keys:
            values.append("%s=%s"%(key, self.__dict__[key]))
        s = s % (self.__class__.__name__, " ".join(values))
        return s


class JiraIssue(Wrappable):
    def _get_status(self):
        return self._server.status_codes[self.__dict__['status']]

    def _set_status(self, v):
        if v in self._server.status_codes:
            self.__dict__['status'] = v
            return
        else:
            for code, status in self._server.status_codes.iteritems():
                if v == status or v == int(code):
                    self.__dict__['status'] = code
                    return
        raise ValueError, "Invalid code/description for a JIRA status."

    status = property(_get_status, _set_status)


class JiraComment(Wrappable):
    pass


class JiraUser(Wrappable):
    def login(self, password):
        """
        Logs the user into JIRA

        >>> authorization_token = user.login()
        """
        return self._server.login(self.name, password)

    def logout(self):
        """
        Logs the user out of JIRA

        >>> success = user.logout()
        """
        return self._server.logout()



class Jira(object):
    """
    """
    interface.implements(ITicketSystem)

    status_codes = {
        "1":"Open",
        "3":"In Progress",
        "4":"Reopened",
        "5":"Resolved",
        "6":"Closed",
        "10001":"Definition",
        "10002":"Planning",
        "10003":"Blocked",
        "10004":"Internal Acceptance Test",
        "10005":"Integration Test",
        "10007":"Approved for Release",
        "10012":"RC Update",
        "10013":"Pending Review",
        "10015":"New Hire Paperwork",
        "10016":"Payroll",
        "10017":"Active",
        "10018":"Access Disabled",
        "10020":"Payroll Disabled",
        "10021":"Pending Termination",
        "10023":"Production",
        "10024":"Escalated",
        "10025":"Notification",
        "10026":"Terminated",
        "10030":"Preliminary Analysis",
        "10031":"Preliminary Review",
        "10032":"Implemented",
        "10033":"Final Review",
        "10034":"Pending Implementation",
        "10041":"Completed",
        "10042":"Pending Textura Action",
        "10043":"Pending Customer Action",
        "10044":"Pending Bank Action",
        "10045":"Pending Test File",
        "10047":"Follow-up",
        "10048":"Active (Pending Payroll Adj.)",
        "10051":"Coding",
        "10053":"Functional Test",
        "10054":"On Hold",
        "10055":"Defective",
        "10056":"Network/HW Setup",
        "10057":"Environment Setup",
        "10058":"Test Log Review",
        }

    def __init__(self,
                 url="http://jira.texturallc.net/rpc/xmlrpc",
                 server=None):
        self.url = url
        if not server:
            server = xmlrpclib.ServerProxy(self.url, encoding="utf-8")
            self._server = server.jira1
        else:
            self._server = server
        self.token = None

    def login(self, username=None, password=None):
        """
        Logs the user into JIRA

        >>> j = Jira()
        >>> String = j.login("username", "password")
        """
        if not username:
            self.username = raw_input("Username:")
        else:
            self.username = username
        if not password:
            self.password = getpass.getpass("Password:")
        else:
            self.password = password
        self.token = self._server.login(self.username,
                                                self.password)
        return self.token

    def logout(self):
        """
        Logs the user out of JIRA

        >>> success = j.logout()
        """
        return self._server.logout(self.token)

    def addComment(self, issueKey="", comment=""):
        """
        Adds a comment to an issue

        >>> success = j.addComment("FEAT-1000", "This is a comment to be added to issue FEAT-1000")
        """
        return_code = self._server.addComment(self.token, issueKey, comment)
        return return_code

    def createIssue(self, rIssueStruct=""):
        """
        Hashtable 	createIssue(String token, Hashtable rIssueStruct)
        Creates an issue in JIRA from a Hashtable object.
        """
        raise NotImplementedError

    def getComments(self, issueKey):
        """
        Returns all comments associated with the issue

        >>> comments = j.getComments("FEAT-1000")
        """
        comments = []
        for c in self._server.getComments(self.token,
                                          issueKey):
            comments.append(JiraComment(self, c))
        return comments

    def getComponents(self, projectKey):
        """
        Returns all components available in the specified project

        >>> components = j.getComponents("FEAT")
        """
        components = self._server.getComponents(self.token,
                                                     projectKey)
        return components

    def getIssue(self, issueKey):
        """
        Gets an issue from a given issue key.

        >>> issue = j.getIssue("FEAT-1")
        >>> print issue["project"]
        FEAT
        """
        try:
            issue_dict = self._server.getIssue(self.token, issueKey)
            issue = JiraIssue(self, issue_dict)
            return issue
        except xmlrpclib.Error, v:
            return None

    def getIssuesFromFilter(self, filterId):
        """
        Vector 	getIssuesFromFilter(String token, String filterId)
        Executes a saved filter

        >>> filterId = "100000"
        >>> issues = j.getIssuesFromFilter(filterId)
        >>> for i in issues:
        >>>     print issues['status']
        10013
        10013
        ...
        """
        issues = []
        for i in self._server.getIssuesFromFilter(self.token,
                                                  filterId):
            issues.append(JiraIssue(self, i))
        return issues

    def getIssuesFromTextSearch(self, searchTerms):
        """
        Vector 	getIssuesFromTextSearch(String token, String searchTerms)
        Find issues using a free text search
        """
        issues = []
        for i in self._server.getIssuesFromTextSearch(self.token,
                                                      searchTerms):
            issues.append(JiraIssue(self, i))
        return issues

    def getIssuesFromTextSearchWithProject(self, projectKeys, searchTerms, maxNumResults=10):
        """
        Vector 	getIssuesFromTextSearchWithProject(String token, Vector projectKeys, String searchTerms, int maxNumResults)
        Find issues using a free text search, limited to certain projects
        """
        if issubclass(type(projectKeys), (str, unicode)):
            keys = [projectKeys,]
        else:
            keys = projectKeys
        issues = []
        for i in self._server.getIssuesFromTextSearchWithProject(self.token,
                                                           keys,
                                                           searchTerms,
                                                           maxNumResults
                                                           ):
            issues.append(JiraIssue(self, i))
        return issues


    def getIssueTypes(self):
        """
        Returns all visible issue types in the system

        >>> issueTypes = j.getIssueTypes()
        >>> for i in issueTypes:
        >>>     ...

        """
        issueTypes = self._server.getIssueTypes(self.token)
        return issueTypes

    def getIssueTypesForProject(self, projectId):
        """
        Returns all visible (non-sub task) issue types for the specified project id

        >>> projectId = 10000
        >>> issueTypes = j.getIssueTypesForProject(projectId)
        """
        issueTypes = self._server.getIssueTypesForProject(self.token, projectId)
        return issueTypes

    def getPriorities(self):
        """
        Returns all priorities in the system

        >>> priorities = j.getPriorities()
        >>> for p in priorities:
        >>>     ...
        """
        priorities = self._server.getPriorities(self.token)
        return priorities

    def getProjects(self):
        """
        Returns a list of projects available to the user

        >>> projects = j.getProjects()
        >>> for p in projects:
        >>>     ...
        """
        projects = self._server.getProjects(self.token)
        return projects

    def getResolutions(self):
        """
        Returns all resolutions in the system

        >>> resolutions = j.getResolutions()
        >>> for r in resolutions:
        >>>     ...
        """
        resolutions = self._server.getResolutions(self.token)
        return resolutions

    def getSavedFilters(self):
        """
        Gets all saved filters available for the currently logged in user

        >>> filters = j.getSavedFilters()
        """
        filters = self._server.getSavedFilters(self.token)
        return filters

    def getServerInfo(self):
        """
        Returns the Server information such as baseUrl, version, edition, buildDate, buildNumber.

        >>> info = j.getServerInfo()
        >>> print "Connected to JIRA server at:",info['baseUrl']
        """
        info = self._server.getServerInfo(self.token)
        return info

    def getStatuses(self):
        """
        Returns all statuses in the system

        >>> statuses = j.getStatuses()
        """
        statuses = self._server.getStatuses(self.token)
        return statuses

    def getSubTaskIssueTypes(self):
        """
        Returns all visible subtask issue types in the system

        >>> issueTypes = j.getSubTaskIssueTypes()
        """
        issueTypes = self._server.getSubTaskIssueTypes(self.token)
        return issueTypes


    def getSubTaskIssueTypesForProject(self, projectId):
        """
        Vector 	getSubTaskIssueTypesForProject(String token, String projectId)
        Returns all visible sub task issue types for the specified project id.
        """
        issueTypes = self._server.getSubTaskIssueTypesForProject(self.token, projectId)
        return issueTypes

    def getUser(self, username):
        """
        Returns a user's information given a username

        >>> user = j.getUser("michael.steder")
        >>> print user['email']
        michael.steder@texturallc.com
        """
        user_info = self._server.getUser(self.token, username)
        #return user_info
        if not user_info:
            user = None
        else:
            user = JiraUser(self, user_info)
        return user

    def getVersions(self, projectKey):
        """
        Returns all versions available in the specified project

        >>> versions = j.getVersions("FEAT")
        """
        versions = self._server.getVersions(self.token, projectKey)
        return versions

    def updateIssue(self, issueKey, fieldValues):
        """
        Hashtable 	updateIssue(String token, String issueKey, Hashtable fieldValues)
        Updates an issue in JIRA from a Hashtable object.
        """
        raise NotImplementedError

class Stickies(object):
    """
    Commandline wrapper for the Jira class.

    Exposes a few basic commands:
      get FEAT-1
      search <search terms>
      login
      logout
      comments FEAT-1
    """
    def __init__(self):
        self.options, self.args = self.getOptions()
        if options.url:
            self.jira = Jira(options.url)
        else:
            self.jira = Jira()

    def getOptions(self):
        parser = optparse.OptionParser(usage="""usage:
$ jira <options> command [arg1, arg2, ...]
""")
        parser.add_option("--url", dest="url", help="URL to JIRA RPC2 Service")
        return parser.parse_args()

    def login_prompt(self, username=None, password=None):
        username = username or raw_input("Username:")
        password = password or getpass.getpass("Password:")
        token = self.jira.login(username, password)
        self.write_token_file(token, username)

    def token_path(self):
        path = os.path.join(os.environ["HOME"],".jira_token")
        return path

    def load_token_or_login(self):
        path  = self.token_path()
        if os.path.exists(path):
            file_contents = open(path,"r").read()
            username, token = file_contents.split()
            self.jira.token = token
            try:
                self.jira.getUser(username)
            except xmlrpclib.Error, e:
                print e
                self.login_prompt()
        else:
            self.login_prompt()

    def write_token_file(self, token, username):
        path = self.token_path()
        file = open(path,"w")
        file.write("%s %s"%(username, token))
        file.close()

    def delete_token_file(self):
        path = self.token_path()
        os.remove(path)

    def run(self):
        if len(self.args)>0:
            if self.args[0] == "login":
                self.login_prompt()
            else:
                self.load_token_or_login()
                if self.args[0] == "get":
                    issueKey = self.args[1]
                    issue = self.jira.getIssue(issueKey)
                    print """Key: %s
Status: %s
Description: %s
""" %( issue.key, issue.status, issue.description )
                elif self.args[0] == "logout":
                    self.jira.logout()
                    self.delete_token_file()
                elif self.args[0] == "status":
                    issueKey = self.args[1]
                    issue = self.jira.getIssue(issueKey)
                    status = self.jira.status_codes[issue["status"]]
                    print "Status of %s: %s"%(issue["key"],
                                              status)
                elif self.args[0] == "comments":
                    issueKey = self.args[1]
                    comments = self.jira.getComments(issueKey)
                    print "Comments for %s:"%(issueKey)
                    print "-"*60
                    comments.sort(key=lambda x: mx.DateTime.DateFrom(x['created']))
                    #comments.sort(key=lambda x: x['author'])
                    for comment in comments:
                        print """Comment by %s at %s (updated by %s at %s)
%s
                        """%(comment['author'], comment['created'],
                             comment['updateAuthor'], comment['updated'],
                             comment['body'])
                    print "-"*60
                elif self.args[0] == "search":
                    searchTerms = " ".join(self.args[1:])
                    issues = self.jira.getIssuesFromTextSearch(searchTerms)
                    issues.sort(key=lambda x: x['key'])
                    print "Issue results for the search: %s"%(searchTerms)
                    print "-"*60
                    for issue in issues:
                        print """%s:
%s
                        """%(issue['key'], issue['description'])
                    print "-"*60
        else:
            pass
